/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package autoUpdate;

import sync.local.FileSystemSyncThread;
import autoUpdate.albumContent.AlbumContentUpdateThread;
import autoUpdate.albumList.AlbumListUpdateThread;
import autoUpdate.albumList.IAlbumListUpdateListener;
import com.google.gdata.data.photos.AlbumEntry;
import configuration.ConfigurationManager;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.DefaultListModel;
import name.pachler.nio.file.StandardWatchEventKind;
import picasaphotomanager.PicasaSessionManager;
import picasaphotomanager.PicasawebClient;
import utilities.IO;

/**
 *
 * @author paskud
 */
public class UpdateManager {
    private static final Map<String, WatchEntry> _watchedAlbums;
    private static AlbumListUpdateThread _updateAlbumThread;
    private static FileSystemSyncThread _updateFilesystemThread;
    private static DefaultListModel _model = new DefaultListModel();

    static
    {
        _watchedAlbums = ConfigurationManager.getUpdateDatabase().getWatchedAlbums();
    }

    public static void WatchAlbum(WatchEntry album)
    {
        String parsedAlbumId = parseAlbumId(album.getId());

        if (_watchedAlbums.containsKey(album.getId()))
            _watchedAlbums.remove(album.getId());

        _watchedAlbums.put(parsedAlbumId, album);
        Logger.getLogger(UpdateManager.class.getName()).log(Level.INFO, "Watching album: {0}", album.getTitle());
        Refresh();
    }

    public static void UnwatchAlbum(String albumId)
    {
        String parsedAlbumId = parseAlbumId(albumId);

        if (_watchedAlbums.containsKey(parsedAlbumId))
        {
            WatchEntry watchedAlbum = _watchedAlbums.get(parsedAlbumId);
            watchedAlbum.setEnabled(false);
            Logger.getLogger(UpdateManager.class.getName()).log(Level.INFO, "Unwatching album: {0}", watchedAlbum.getTitle());
            Refresh();
        }
    }

    public static void SaveWatchedFolders()
    {
        ConfigurationManager.getUpdateDatabase().setWatchedAlbums(_watchedAlbums);
    }

    public static void StartWatching(IAlbumListUpdateListener listener)
    {
        _updateAlbumThread = new AlbumListUpdateThread();
        _updateAlbumThread.setName("Album list update thread");
        
        _updateAlbumThread.addAlbumListUpdateListiner(listener);
        _updateAlbumThread.start();
        
        
        _updateFilesystemThread = new FileSystemSyncThread();
        _updateFilesystemThread.setName("Local filesystem watcher");
        _updateFilesystemThread.start();
    }

    public static void StopWatching()
    {
        _updateAlbumThread.setEnabled(false);
        _updateAlbumThread.interrupt();
        _updateAlbumThread = null;
        
        _updateFilesystemThread.setEnabled(false);
        _updateFilesystemThread.interrupt();
        _updateFilesystemThread = null;
    }
    
    public static Boolean IsWatching() {
        return _updateAlbumThread != null ? _updateAlbumThread.isEnabled() : false;
    }
    
    public static void addAlbumUpdateAvailableListiner(IAlbumListUpdateListener listener)
    {
        _updateAlbumThread.addAlbumListUpdateListiner(listener);
    }

    public static boolean IsWatched(String albumId) {
        WatchEntry watchedAlbum = getWatchEntry(albumId);

        if (watchedAlbum != null) {
            return watchedAlbum.isEnabled();
        }

        return false;
    }
    
    public static void UpdateAlbumContent(AlbumEntry album)
    {
        Logger.getLogger(UpdateManager.class.getName()).log(Level.INFO, "Updating content of the album with id {0}", album.getTitle().getPlainText());
        WatchEntry watchedAlbum = getWatchEntry(album.getId());

        watchedAlbum.setLastUpdatedLocally(new Date(album.getUpdated().getValue()));
        if (watchedAlbum.isDownloadOnce())
            UnwatchAlbum(album.getId());


        CreateAlbumFolder(album.getId());
        
        AlbumContentUpdateThread updateAlbumContentThread = new AlbumContentUpdateThread();
        
        updateAlbumContentThread.setName("Album content update thread");
        updateAlbumContentThread.setAlbum(album);
        updateAlbumContentThread.start();
    }

    private static void CreateAlbumFolder(String albumId) {
        String albumPath = getWatchEntry(albumId).getLocalPath();
        File albumFile = new File(albumPath);

        //Create album folder
        if (!albumFile.exists()) {
            albumFile.mkdirs();
            _updateFilesystemThread.AddWatchPath(albumFile.getPath());
        }
    }

    private static String parseAlbumId(String albumId) {
        return albumId.substring(albumId.lastIndexOf("/")+1);
    }

    public static WatchEntry getWatchEntry(String albumId) {
        String parsedAlbumId = parseAlbumId(albumId);

        if (_watchedAlbums.containsKey(parsedAlbumId))
            return _watchedAlbums.get(parsedAlbumId);
        
        return null;
    }

    /**
     * @return the _model
     */
    public static DefaultListModel getModel() {
        return _model;
    }

    /**
     * @param aModel the _model to set
     */
    public static void setModel(DefaultListModel aModel) {
        _model = aModel;
    }

    public static void Refresh() {
        _updateAlbumThread.interrupt();
    }
   
    public static void UpdateMedia(WatchEntry watchPhoto) {
        File photoFile = new File(watchPhoto.getLocalPath());
        
        WatchEntry watchAlbum = null;

        String photoFilename = "unknown";
        if (watchPhoto.getMediaUrl() != null) {
            //Set the file filename
            photoFilename = getUrlFileName(watchPhoto.getMediaUrl());

            watchAlbum = getWatchEntry(watchPhoto.getParentId());
        } else if (photoFile != null) {
            photoFilename = photoFile.getName();

            watchAlbum = GetAlbumByPath(photoFile.getParentFile().getPath());
        }


        UPDATE_ACTION action = CompareMedia(watchPhoto);

        switch (action) {
            case UPDATE_LOCAL:
                try {
                    URL photoUrl = new URL(watchPhoto.getMediaUrl());
                    
                    _updateFilesystemThread.SupressEvent(photoFile.getPath(), StandardWatchEventKind.ENTRY_CREATE);
                    _updateFilesystemThread.SupressEvent(photoFile.getPath(), StandardWatchEventKind.ENTRY_MODIFY);

                    if (IO.SaveUrlToFile(photoFile, photoUrl, true))
                        Logger.getLogger(AlbumContentUpdateThread.class.getName()).log(Level.INFO, "Updated picture {0}", photoFilename);
                    else
                        Logger.getLogger(AlbumContentUpdateThread.class.getName()).log(Level.INFO, "Unexpected error while downloading picture {0}", photoFilename );


                    //Update database
                    watchPhoto.setLastUpdatedLocally(new Date(photoFile.lastModified()));
                    URL thumbnailUrl = new URL(watchPhoto.getThumbnailUrl());
                    String newHash = getHashForUrlResource(thumbnailUrl);
                    watchPhoto.setHash(newHash);
                    
                } catch (MalformedURLException ex) {
                    Logger.getLogger(UpdateManager.class.getName()).log(Level.SEVERE, "Incorrect file URL while attempting to update local file", ex);
                }
                break;
            case DELETE_LOCAL:              
                if (photoFile.delete()) {
                    Logger.getLogger(AlbumContentUpdateThread.class.getName()).log(Level.INFO, "Removed local picture {0}", photoFilename);
                    watchAlbum.getChildren().remove(watchPhoto.getId());
                } else {
                    Logger.getLogger(AlbumContentUpdateThread.class.getName()).log(Level.INFO, "Error while deleting local picture {0}", photoFilename);
                }
                break;
            case UPDATE_ONLINE:
                Logger.getLogger(AlbumContentUpdateThread.class.getName()).log(Level.SEVERE, "Action UPDATE_ONLINE is not yet implemented");
                break;
            case DELETE_ONLINE:
                PicasawebClient client = PicasaSessionManager.GetClient(watchPhoto.getAlbumOwner());
                client.deleteMedia(watchPhoto.getId());
                Logger.getLogger(AlbumContentUpdateThread.class.getName()).log(Level.SEVERE, "Action DELETE_ONLINE is not yet implemented");
                break;
            case MERGE:
                Logger.getLogger(AlbumContentUpdateThread.class.getName()).log(Level.SEVERE, "Action MERGE is not yet implemented");
                break;
            case ERROR:
                Logger.getLogger(AlbumContentUpdateThread.class.getName()).log(Level.INFO, "Unexpected error while trying to synchronise file", photoFilename);
                break;
            case UP_TO_DATE:
                Logger.getLogger(AlbumContentUpdateThread.class.getName()).log(Level.SEVERE, "Action UP_TO_DATE is not yet implemented");
                break;

        }

    }

    private static String getHashForUrlResource(URL photoUrl) {
        try {
            URLConnection connection = photoUrl.openConnection();
            InputStream stream = photoUrl.openStream();
            int contentLength = connection.getContentLength();
            byte[] buffor = new byte[contentLength];
            int bytesRead = 0;
            int offset = 0;

            while (offset < contentLength) {
                bytesRead = stream.read(buffor, offset, contentLength-offset);

                if (bytesRead == -1) break;

                offset += bytesRead;
            }

            System.out.println("Hash image size: " + buffor.length + " bytes");
            MessageDigest hashGenerator = MessageDigest.getInstance("MD5");
            return IO.byteArrayToHexString(hashGenerator.digest(buffor));
        } catch (NoSuchAlgorithmException ex) {
            Logger.getLogger(UpdateManager.class.getName()).log(Level.SEVERE, null, ex);
        } catch (MalformedURLException ex) {
            Logger.getLogger(UpdateManager.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IOException ex) {
            Logger.getLogger(UpdateManager.class.getName()).log(Level.SEVERE, null, ex);
        }

        return null;
    }

    private static UPDATE_ACTION CompareMedia(WatchEntry watchEntry) {
        File photoFile = new File(watchEntry.getLocalPath());

        String photoFilename = "unknown";
        boolean existOnline = (watchEntry.getMediaUrl() != null);
        boolean existLocal = photoFile.exists();


        if (existOnline) {
            //Set the file filename
            photoFilename = getUrlFileName(watchEntry.getMediaUrl());
        } else if (existLocal) {
            photoFilename = photoFile.getName();
        }


        boolean existDb = (watchEntry.getHash() != null);

        boolean changedLocal = false;
        boolean changedOnline = false;

        if (existDb && existLocal) {
            changedLocal = photoFile.lastModified() != watchEntry.getLastUpdatedOnline().getTime();
        }

        if (existDb && existOnline) {
            try {
                String dbHash = watchEntry.getHash();
                URL thumbnailUrl = new URL(watchEntry.getThumbnailUrl());
                String onlineHash = getHashForUrlResource(thumbnailUrl);
                changedOnline = !dbHash.equals(onlineHash);
            } catch (MalformedURLException ex) {
                Logger.getLogger(UpdateManager.class.getName()).log(Level.SEVERE, "Cannot download photo thumbnail for compare hash calculation.", ex);
            }
        }


        //handles updates
        if (existDb) {
            //Both online and local files exist
            if (existLocal && existOnline) {
                if (!changedLocal && !changedOnline) {
                    Logger.getLogger(UpdateManager.class.getName()).log(Level.INFO, "Photo {0} exists in DB, locally and online; no changes online and locally; Action: None", photoFilename);
                    return UPDATE_ACTION.UP_TO_DATE;
                }
                else if (!changedLocal && changedOnline) {
                    Logger.getLogger(UpdateManager.class.getName()).log(Level.INFO, "Photo {0} exists in DB, locally and online; changes online but not locally; Action: Update Local", photoFilename);
                    return UPDATE_ACTION.UPDATE_LOCAL;
                }
                else if (changedLocal && !changedOnline) {
                    Logger.getLogger(UpdateManager.class.getName()).log(Level.INFO, "Photo {0} exists in DB, locally and online; changes locally but not online; Action: Update Online", photoFilename);
                    return UPDATE_ACTION.UPDATE_ONLINE;
                }
                else if (changedLocal && changedOnline) {
                    Logger.getLogger(UpdateManager.class.getName()).log(Level.INFO, "Photo {0} exists in DB, locally and online; changes locally and online; Action: Merge", photoFilename);
                    return UPDATE_ACTION.MERGE;
                }
            //file removed locally
            } else if (!existLocal && existOnline) {
                if (!changedOnline) {
                    Logger.getLogger(UpdateManager.class.getName()).log(Level.INFO, "Photo {0} exists in DB and online but doesn't exist locally; it was not changed online; Action: Delete online", photoFilename);
                    return UPDATE_ACTION.DELETE_ONLINE;
                }
                else {
                    Logger.getLogger(UpdateManager.class.getName()).log(Level.INFO, "Photo {0} exists in DB and online but doesn't exist locally; it was changed online; Action: Merge", photoFilename);
                    return UPDATE_ACTION.MERGE;
                }
            //removed online
            } else if (existLocal && !existOnline) {
                if (!changedLocal) {
                    Logger.getLogger(UpdateManager.class.getName()).log(Level.INFO, "Photo {0} exists in DB and locally but doesn't exist online; it was not changed locally; Action: Delete Local", photoFilename);
                    return UPDATE_ACTION.DELETE_LOCAL;
                }
                else {
                    Logger.getLogger(UpdateManager.class.getName()).log(Level.INFO, "Photo {0} exists in DB and locally but doesn't exist online; it was changed locally; Action: Merge", photoFilename);
                    return UPDATE_ACTION.MERGE;
                }
            //database out of date
            } else if (!existLocal && !existOnline) {
                Logger.getLogger(UpdateManager.class.getName()).log(Level.INFO, "Photo {0} exists in DB only; Action: None", photoFilename);
                return UPDATE_ACTION.UP_TO_DATE;
            }
        } //handles initial download
        else if (!existDb) {
            if (existLocal && existOnline) {
                //download full-ress photo and compare to the local
                try {
                    boolean localMatchesOnline = (photoFile.getTotalSpace() == watchEntry.getOnlineSize());

                    if (localMatchesOnline) {
                        URL photoUrl = new URL(watchEntry.getMediaUrl());

                        String onlineHash = getHashForUrlResource(photoUrl);
                        String localHash = getHashForUrlResource(photoFile.toURI().toURL());

                        localMatchesOnline = (onlineHash.equals(localHash));
                    }

                    if (localMatchesOnline) {
                        //ToDo: update DB
                        Logger.getLogger(UpdateManager.class.getName()).log(Level.INFO, "Photo {0} doesn't exist in DB but exist locally and online. Local copy matches online version; Action: None", photoFilename);
                        return UPDATE_ACTION.UP_TO_DATE;
                    } else {
                        Logger.getLogger(UpdateManager.class.getName()).log(Level.INFO, "Photo {0} doesn't exist in DB but exist locally and online. Local copy doesn't matche online version; Action: Merge", photoFilename);
                        return UPDATE_ACTION.MERGE;
                    }
                } catch (MalformedURLException ex) {
                    Logger.getLogger(UpdateManager.class.getName()).log(Level.SEVERE, "Cannot download full-res photo for hash compare operation.", ex);
                }
            }

            //initial download of the new online file
            if (existOnline && !existLocal) {
                Logger.getLogger(UpdateManager.class.getName()).log(Level.INFO, "Photo {0} doesn't exist in DB and locally but exist online; Action: Update Local", photoFilename);
                return UPDATE_ACTION.UPDATE_LOCAL;
            }

            //initial upload of the new local file
            if (!existOnline && existLocal) {
                Logger.getLogger(UpdateManager.class.getName()).log(Level.INFO, "Photo {0} doesn't exist in DB and online but exist locally; Action: Update Online", photoFilename);
                return UPDATE_ACTION.UPDATE_ONLINE;
            }
        }


        Logger.getLogger(UpdateManager.class.getName()).log(Level.SEVERE, "Exiting through last chance return statement. This could indicate bug.");
        return UPDATE_ACTION.UP_TO_DATE;
    }


    public static WatchEntry GetAlbumByPath(String albumPath) {
        for (WatchEntry _watchedAlbum : _watchedAlbums.values())
            if (_watchedAlbum.getLocalPath().equals(albumPath)) return _watchedAlbum;
           
        return null;
    }
    
    public static WatchEntry GetPhotoByPath(String localPath) {
        String albumPath = (new File(localPath)).getParentFile().getPath();
        WatchEntry watchedAlbum = GetAlbumByPath(albumPath);
        
        for (WatchEntry watchPhoto : watchedAlbum.getChildren().values())
            if (watchPhoto.getLocalPath().equals(localPath)) return watchPhoto;
           
        return null;
    }    

    public static String getUrlFileName(String url) {
        String photoUrlFileName = (new File(url)).getName();
        String decodedUrlFileName = null;
                
        try {
            decodedUrlFileName = java.net.URLDecoder.decode(photoUrlFileName, "UTF-8");
            decodedUrlFileName = java.net.URLDecoder.decode(decodedUrlFileName, "UTF-8");
            decodedUrlFileName = java.net.URLDecoder.decode(decodedUrlFileName, "UTF-8");
        } catch (UnsupportedEncodingException ex) {
            Logger.getLogger(UpdateManager.class.getName()).log(Level.SEVERE, "Cannot decode URL filename", ex);
        }

        return decodedUrlFileName;
    }




}
